package simulation;

import assembly.Assembly;
import assembly.Mass;
import assembly.Spring;
import force.Force;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import util.Sprite;
import view.Canvas;


/**
 * XXX. Known bug: sometimes masses find way through walls
 * 
 * @author Robert C. Duvall
 * @author Scott Valentine
 * @author Dagbedji Fagnisse
 */
public class Model {

    /** current assemblies in the simulation */
    private List<Assembly> myAssemblies;

    /** current forces in the simulation (both toggled on and toggled off */
    private List<Force> myGlobalForces;

    /** view of the simulation */
    private Canvas myView;

    /** the current location of the walls */
    private Dimension myWalls;

    /** control between user inputs on the keyboard and model */
    private UserInteraction myInteractor;

    /**
     * Create a game of the given size with the given display for its shapes.
     * 
     * @param canvas the canvas used by this model to render elements.
     */
    public Model (Canvas canvas) {
        myView = canvas;
        myAssemblies = new ArrayList<Assembly>();
        myGlobalForces = new ArrayList<Force>();
        myWalls = canvas.getSize();
        myInteractor = new UserInteraction(this, myView);
    }

    /**
     * Add given assembly to this simulation.
     * 
     * @param a is the assembly to be added to simulation
     */
    private void add (Assembly a) {
        a.setForces(myGlobalForces);
        myAssemblies.add(a);
    }

    /**
     * Add given force to this simulation.
     * 
     * @param force to be added to simulation
     */
    private void add (Force force) {
        myGlobalForces.add(force);
    }

    /**
     * changes the current forces in the simulation
     * 
     * @param forces - forces now acting in the simulation
     */
    public void add (List<Force> forces) {
        myGlobalForces = forces;
        syncAssemblyAndForces();
    }

    /**
     * adds an object to the simulation
     * 
     * @param load - the object to be added to the simulation
     */
    public void add (Object load) {
        if (load instanceof Assembly) {
            add((Assembly) load);
        }
        else if (load instanceof Force) {
            add((Force) load);
        }
        syncAssemblyAndForces();
    }

    private void syncAssemblyAndForces () {
        for (Assembly a : myAssemblies) {
            a.setForces(myGlobalForces);
        }
    }

    /**
     * clears all sprites in the simulation
     */
    public void clearAssemblies () {
        myAssemblies.clear();
    }

    /**
     * gives the current assemblies in the simulation
     * 
     * @return a list of the assemblies in the simulation
     */
    public List<Assembly> getAssemblies () {
        return myAssemblies;
    }

    /**
     * Get the bounds of the walls associated with this model.
     * 
     * @return bounds of this model (potentially different from those of the canvas)
     */
    public Dimension getBounds () {
        return myWalls;
    }

    /** TODO **/
    private Mass getClosestMassTo (Point2D p) {

        List<Mass> allOfTheMasses = new ArrayList<Mass>();
        for (Assembly a : myAssemblies) {
            allOfTheMasses.addAll(a.getMasses());
        }
        Mass closestMass = Collections.min(allOfTheMasses, new ProximityComparator(p));
        return closestMass;
    }

    /**
     * gets a list of the current forces in the simulation
     * 
     * @return a list of the forces used in the simulation
     */
    public List<Force> getForces () {
        return myGlobalForces;
    }

    /**
     * returns a list of masses in a given assembly in the simulation
     * 
     * @param assemblyID - the id of the assembly to get the masses from
     * @return a list of all the masses in the simulation
     */
    public List<Mass> getMasses (int assemblyID) {
        return myAssemblies.get(assemblyID).getMasses();
    }

    /**
     * Draw all elements of the simulation.
     * 
     * @param pen used to paint.
     *        draws the sprites in the simulation in the window
     */
    public void paint (Graphics2D pen) {
        paintAssemblies(pen);
    }

    private void paintAssemblies (Graphics2D pen) {
        for (Assembly a : myAssemblies) {
            a.paint(pen);
        }
    }

    /**
     * updates the simulation based on the elapsed time since the last update
     * 
     * @param elapsedTime the amount of time that has passed since the last update
     */
    public void update (double elapsedTime) {
        myInteractor.checkInputs();
        Dimension bounds = getBounds();
        updateAssemblies(elapsedTime, bounds);

    }

    /**
     * update all of the assemblies
     * 
     * @param elapsedTime is the time since last update
     * @param bounds is the current position of the walls
     */
    private void updateAssemblies (double elapsedTime, Dimension bounds) {
        for (Assembly a : myAssemblies) {
            a.update(elapsedTime, bounds);
        }
    }

    /**
     * updates the position of the walls argument
     * 
     * @param n the amount of change of the position of the walls
     */
    public void updateBounds (int n) {
        myWalls.setSize(myWalls.getWidth() + n, myWalls.getHeight() + n);
    }

    /**
     * determines if a mouse is currently active in the simulation
     * 
     * @return - boolean representing if a mouse is currently present
     */
    public boolean assembliesContainMouseElements () {
        for (Assembly a : myAssemblies) {
            if (a instanceof MouseElements) { return true; }
        }
        return false;
    }

    /**
     * removes a mouse element from the simulation
     */
    public void removeMouseElements () {
        for (Assembly a : myAssemblies) {
            if (a instanceof MouseElements) {
                myAssemblies.remove(a);
                return;
            }
        }
    }

    /**
     * add a new mouse element to the simulation
     */
    public void addMouseElements () {
        add(new MouseElements());
    }

    /**
     * Special type of assembly handling interactivity with the mouse.
     * 
     * @author Dagbedji Fagnisse
     * 
     */
    private class MouseElements extends Assembly {
        private Spring mySpring;
        private Mass myMass;

        private MouseElements (Point p, int i, Mass m) {
            myMass = new Mass(p, i);
            mySpring = new Spring(myMass, m, 0, Spring.DEFAULT_K);
        }

        public MouseElements () {
            this((Point) myView.getLastMousePosition(), 0,
                 getClosestMassTo(myView.getLastMousePosition()));
        }

        public void paint (Graphics2D pen) {
            mySpring.paint(pen);
        }

        @Override
        public void update (double elapsedTime, Dimension bounds) {
            updateMassLocation();
            mySpring.update(elapsedTime, bounds);
        }

        private void updateMassLocation () {
            Point2D mousePos = myView.getLastMousePosition();
            myMass.setCenter(mousePos.getX(), mousePos.getY());
        }

    }

    /**
     * Comparator intended to compare a collection of Sprites based on their
     * distance from a specific point.
     * 
     * @author Dagbedji Fagnisse
     * 
     */
    public class ProximityComparator implements Comparator<Sprite> {
        /** the current position of the mouse */
        private Point2D myMousePosition;

        /**
         * default constructor, creates based on last mouse position
         */
        public ProximityComparator () {
            this(myView.getLastMousePosition());
        }

        /**
         * creates a new ProximityComparator based on a given point
         * 
         * @param p - point at which the ProximityComparator is calculated
         */
        public ProximityComparator (Point2D p) {
            myMousePosition = p;
        }

        /**
         * compares the distance between two given sprites
         * 
         * @param s1 sprite to be compared by distance
         * @param s2 other sprite to be compared by distance
         */
        public int compare (Sprite s1, Sprite s2) {
            return (int) (s1.distance(myMousePosition) - s2.distance(myMousePosition));
        }
    }
}
